package org.log4jconfig.xml.editor.validation;

import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator;
import org.eclipse.wst.validation.internal.core.ValidationException;
import org.eclipse.wst.validation.internal.provisional.core.IReporter;
import org.eclipse.wst.validation.internal.provisional.core.IValidationContext;
import org.eclipse.wst.validation.internal.provisional.core.IValidator;
import org.eclipse.wst.xml.core.internal.parser.regions.EndTagOpenRegion;
import org.log4jconfig.xml.editor.Log4jConfigXmlEditorIntegrationPlugin;
import org.log4jconfig.xml.editor.utils.JdtUtils;
import org.log4jconfig.xml.editor.utils.Logger;
import org.log4jconfig.xml.editor.utils.StructuredDocumentRegionUtils;

@SuppressWarnings("restriction")
public class ContentValidator implements IValidator, ISourceValidator {

	public static final String DEFAULT_MARKER_ID = Log4jConfigXmlEditorIntegrationPlugin.PLUGIN_ID + ".defaultMarker";

	public static final String DEPRECATED_ELEMENT_MARKER_ID = Log4jConfigXmlEditorIntegrationPlugin.PLUGIN_ID
			+ ".deprecatedElementMarker";

	public static final String DEPRECATED_ELEMENT_PROBLEM_TYPE = Log4jConfigXmlEditorIntegrationPlugin.PLUGIN_ID
			+ ".deprecatedElementProblemType";
	private IStructuredDocument document = null;

	public void cleanup(IReporter reporter) {
	}

	public void validate(IValidationContext helper, IReporter reporter) throws ValidationException {
	}

	public void connect(IDocument document) {
		this.document = (IStructuredDocument) document;
	}

	public void disconnect(IDocument document) {
		document = null;
	}

	public void validate(IRegion dirtyRegion, IValidationContext helper, IReporter reporter) {
		try {
			IStructuredDocumentRegion[] dirtyRegions = document.getStructuredDocumentRegions(dirtyRegion.getOffset(),
					dirtyRegion.getLength());
			for (IStructuredDocumentRegion region : dirtyRegions) {
				// if the current region is an </end tag>, it cannot contain any
				// attribute, thus validation is useless
				if (region.getRegions().get(0) instanceof EndTagOpenRegion) {
					continue;
				}
				// remove old markers on this region
				deleteMarkersOnDirtyRegion(document, region);
				String tagName = StructuredDocumentRegionUtils.extractTagName(region);

				if ("logger".equals(tagName)) {
					validateLoggerNameValue(document, region);
				} else if ("appender".equals(tagName)) {
					validateAppenderClassValue(document, region);
				} else if ("layout".equals(tagName)) {
					validateLayoutClassValue(document, region);
				} else if ("param".equals(tagName)) {
					validateParamNameValue(document, region);
				} else if ("level".equals(tagName)) {
					validateLevelValueValue(document, region);
				} else if ("category".equals(tagName)) {
					validateCategoryElement(document, region);
				} else if ("priority".equals(tagName)) {
					validatePriorityElement(document, region);
				}
			}
		} catch (CoreException e) {
			Logger.error("Error while validating dirty region:" + e.getMessage(), e);
		}

	}

	/**
	 * Remove all markers of our plugin type on the given dirty region
	 * 
	 * @param structuredDocument
	 * @param dirtyRegion
	 * @return
	 * @throws CoreException
	 */
	protected void deleteMarkersOnDirtyRegion(IStructuredDocument structuredDocument,
			IStructuredDocumentRegion dirtyRegion) throws CoreException {
		for (String markerType : new String[] { DEFAULT_MARKER_ID, DEPRECATED_ELEMENT_MARKER_ID }) {
			// remove old markers
			int dirtyRegionStartLine = structuredDocument.getLineOfOffset(dirtyRegion.getStart());
			int dirtyRegionEndOffset = dirtyRegion.getEndOffset();
			int dirtyRegionStartOffset = dirtyRegion.getStartOffset();
			IFile resource = JdtUtils.getFile(structuredDocument);

			IMarker[] markers = resource.findMarkers(markerType, true, IResource.DEPTH_INFINITE);
			for (IMarker marker : markers) {
				int markerLineNumber = marker.getAttribute(IMarker.LINE_NUMBER, 0) - 1;
				int markerStartChar = marker.getAttribute(IMarker.CHAR_START, 0);
				int markerEndChar = marker.getAttribute(IMarker.CHAR_END, 0);
				if (markerLineNumber == dirtyRegionStartLine && markerStartChar <= dirtyRegionEndOffset
						&& markerEndChar > dirtyRegionStartOffset) {
					marker.delete();
				}
			}
		}
	}

	/**
	 * Add a marker on the given dirty region
	 * 
	 * @param document
	 * @param problemType
	 * @param value
	 * @param startLine
	 * @throws CoreException
	 */
	protected void addMarkerOnDirtyRegion(String type, IStructuredDocument document,
			IStructuredDocumentRegion dirtyRegion, int severity, String message, String problemType)
			throws CoreException {
		int startLine = document.getLineOfOffset(dirtyRegion.getStart());
		IMarker marker = JdtUtils.getFile(document).createMarker(type);
		marker.setAttribute(IMarker.SEVERITY, severity);
		if (problemType != null) {
			marker.setAttribute("problemType", problemType);
		}
		marker.setAttribute(IMarker.LINE_NUMBER, startLine + 1);
		marker.setAttribute(IMarker.CHAR_START, dirtyRegion.getStartOffset());
		marker.setAttribute(IMarker.CHAR_END, dirtyRegion.getEndOffset());
		marker.setAttribute(IMarker.MESSAGE, message);
	}

	protected void validateAppenderClassValue(IStructuredDocument structuredDocument,
			IStructuredDocumentRegion dirtyRegion) throws CoreException {
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "class");
		if (value == null) {
			// no need to add a marker, XML editor already provides one...
			return;
		}
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		// check for classes (source or binary)
		if (value != null) {
			IType type = javaProject.findType(value, new NullProgressMonitor());
			if (type != null) {
				IType appenderBaseType = JdtUtils.getTypeFromCurrentProject(javaProject,
						"org.apache.log4j.AppenderSkeleton");
				ITypeHierarchy hierarchy = JdtUtils.getTypeHierarchy(javaProject, appenderBaseType);
				if (hierarchy.contains(type)) {
					return;
				}
			}
		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_ERROR, "Class '"
				+ value + "' does not exist in the project's classpath", null);
	}

	protected void validateLayoutClassValue(IStructuredDocument structuredDocument,
			IStructuredDocumentRegion dirtyRegion) throws CoreException {
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "class");
		if (value == null) {
			Logger.warn("Unable to find value for 'name' attribute. Bad XML syntax ?");
			return;
		}
		// check for classes (source or binary)
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		if (javaProject.findType(value, new NullProgressMonitor()) != null) {
			// check for classes (source or binary)
			if (value != null) {
				IType type = javaProject.findType(value, new NullProgressMonitor());
				if (type != null) {
					IType appenderBaseType = JdtUtils.getTypeFromCurrentProject(javaProject, "org.apache.log4j.Layout");
					ITypeHierarchy hierarchy = JdtUtils.getTypeHierarchy(javaProject, appenderBaseType);
					if (hierarchy.contains(type)) {
						return;
					}
				}
			}

		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_ERROR, "Class '"
				+ value + "' does not exist in the project's classpath", null);
	}

	protected void validateLoggerNameValue(IStructuredDocument structuredDocument, IStructuredDocumentRegion dirtyRegion)
			throws CoreException {
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "name");
		if (value == null) {
			Logger.warn("Unable to find value for 'name' attribute. Bad XML syntax ?");
			return;
		}
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		// check for packages first
		List<String> packages = JdtUtils.getPackagesFromFragmentRoots(javaProject.getAllPackageFragmentRoots());
		if (packages.contains(value)) {
			return;
		}
		// check for classes (source or binary)
		if (javaProject.findType(value, new NullProgressMonitor()) != null) {
			return;
		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
				"Logger name '" + value + "' does not match any package or class in the project's classpath", null);

	}

	protected void validateParamNameValue(IStructuredDocument structuredDocument, IStructuredDocumentRegion dirtyRegion)
			throws CoreException {
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		IStructuredDocumentRegion parentElement = dirtyRegion.getPrevious();
		String tagName = StructuredDocumentRegionUtils.extractTagName(parentElement);
		while (parentElement != null && !("appender".equals(tagName) || "layout".equals(tagName))) {
			parentElement = parentElement.getPrevious();
			tagName = StructuredDocumentRegionUtils.extractTagName(parentElement);
		}

		if (parentElement == null) {
			Logger.warn("Unable to locate parent node in the XML tree.");
			return;
		}

		String clazz = StructuredDocumentRegionUtils.extractAttributeValue(parentElement, "class");
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "name");
		// retrieve type given its qualified name
		IType type = javaProject.findType(clazz);
		Set<String> methods = JdtUtils.getSettableAttributes(type);
		if (!methods.contains(value)) {
			addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
					"Param name '" + value + "' does not match any attribute of the appender's or layout's class name",
					null);
		}
		int counter = 0;
		IStructuredDocumentRegion nextRegion = parentElement.getNext();
		String closeTagName = tagName;
		tagName = StructuredDocumentRegionUtils.extractTagName(nextRegion);
		while (!closeTagName.equals(tagName)) {
			if ("param".equals(tagName)) {
				String nameValue = StructuredDocumentRegionUtils.extractAttributeValue(nextRegion, "name");
				if (nameValue.equals(value)) {
					counter++;
				}
			}

			nextRegion = nextRegion.getNext();
			tagName = StructuredDocumentRegionUtils.extractTagName(nextRegion);
		}
		if (counter > 1) {
			addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
					"Param name '" + value + "' is already set for this appender or layout", null);
		}
	}

	protected void validateLevelValueValue(IStructuredDocument structuredDocument, IStructuredDocumentRegion dirtyRegion)
			throws CoreException {
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "value");
		if (value == null) {
			return;
		}
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		IType type = JdtUtils.getTypeFromCurrentProject(javaProject, "org.apache.log4j.Level");
		if (type == null) {
			Logger.warn("Could not find type 'org.apache.log4j.Level' in project's classpath");
			return;
		}
		value = value.toUpperCase();
		List<IField> fields = JdtUtils.getStaticFieldsFromType(type);
		for (IField field : fields) {
			if (field.getElementName().equals(value)) {
				return;
			}
		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, document, dirtyRegion, IMarker.SEVERITY_ERROR, "Level value '"
				+ value + "' is not valid", null);
	}

	protected void validatePriorityElement(IStructuredDocument structuredDocument, IStructuredDocumentRegion dirtyRegion)
			throws CoreException {
		addMarkerOnDirtyRegion(DEPRECATED_ELEMENT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
				"Element 'priority' is deprecated. Use 'level' instead", DEPRECATED_ELEMENT_PROBLEM_TYPE);
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "value").toUpperCase();
		if (value == null) {
			Logger.warn("Unable to find value for 'name' attribute. Bad XML syntax ?");
			return;
		}
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		IType type = JdtUtils.getTypeFromCurrentProject(javaProject, "org.apache.log4j.Priority");
		if (type == null) {
			Logger.warn("Could not find type 'org.apache.log4j.Priority' in project's classpath");
			return;
		}
		List<IField> fields = JdtUtils.getStaticFieldsFromType(type);
		for (IField field : fields) {
			if (field.getElementName().equals(value)) {
				return;
			}
		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, document, dirtyRegion, IMarker.SEVERITY_ERROR, "Priority value '"
				+ value + "' is not valid", null);
	}

	protected void validateCategoryElement(IStructuredDocument structuredDocument, IStructuredDocumentRegion dirtyRegion)
			throws CoreException {
		addMarkerOnDirtyRegion(DEPRECATED_ELEMENT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
				"Element 'category' is deprecated. Use 'logger' instead", DEPRECATED_ELEMENT_PROBLEM_TYPE);
		String value = StructuredDocumentRegionUtils.extractAttributeValue(dirtyRegion, "name");
		if (value == null) {
			Logger.warn("Unable to find value for 'name' attribute. Bad XML syntax ?");
			return;
		}
		IJavaProject javaProject = JdtUtils.getJavaProject(structuredDocument);
		// check for packages first
		List<String> packages = JdtUtils.getPackagesFromFragmentRoots(javaProject.getAllPackageFragmentRoots());
		if (packages.contains(value)) {
			return;
		}
		// check for classes (source or binary)
		if (javaProject.findType(value, new NullProgressMonitor()) != null) {
			return;
		}
		addMarkerOnDirtyRegion(DEFAULT_MARKER_ID, structuredDocument, dirtyRegion, IMarker.SEVERITY_WARNING,
				"Category name '" + value + "' does not match any package or class in the project's classpath", null);
	}

}
